# This file is part of Neuroinfo Toolkit.
#
# Neuroinfo Toolkit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Neuroinfo Toolkit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Neuroinfo Toolkit.  If not, see <http://www.gnu.org/licenses/>.

import os
import sys
import re
import traceback
from neuro.base import Object
import neuro.system as system
import neuro.filesystem as filesystem
import neuro.config as config
from neuro.exceptions import IllegalArgumentException
from neuro.exceptions import IOException
from time import strftime
from pytz import timezone

def iso8601_timestamp(tz=None):
	'''
	ISO 8601 timestamp. Default timezone is neuro.config.timezone
	
	:returns: ISO 8601 timestamp.
	:rtype: str
	'''
	if not tz:
		tz = timezone(tz)
	else:
		tz = timezone(config.timezone)
		
	return datetime.now(tz).isoformat()

def isNumeric(num):
	'''
	Test if input value is numeric or can be cast to a number ::
		
		>>> isNumeric("1")
		True
		>>> isNumeric("1.1")
		True
		>>> isNumeric(1.1)
		True
		>>> isNumeric("a")
		False

	:param num: Number to test
	:type num: int, float, str, ...
	:rtype: bool
	'''
	if(isinstance(num, (float, int, long))):
		return True

	try:
		float(num)
		return True
	except ValueError, e:
		return False
	except TypeError, e:
		return False

def isInteger(num):
	'''
	Test if input value is an integer or can be cast to an integer ::

		>>> isInteger("1")
		True
		>>> isInteger(1.1)
		False
	
	:param num: Input value
	:type num: int, float, str, ...		
	:rtype: bool
	'''
	if(isinstance(num, (int, long))):
		return True
	elif(isinstance(num, basestring)):
		if(re.match(r'^[+-]?[0-9]+$', num)):
			return True

	return False

def isFloat(num):
	'''
	Test if input value is a float or can be cast to a float ::

		>>> isFloat("1.2")
		True
		>>> isFloat("a")
		False
		
	:param num: Input value
	:type num: int, float, str, ...
	:rtype: bool
	'''
	if(isinstance(num, float)):
		return True

	if(isinstance(num, basestring)):
		if(re.match(r'^[+-]?[0-9]*\.[0-9]*$', num)):
			return True

	return False

def isString(string):
	'''
	Test if a variable is a string ::

		>>> isString("abc")
		True
		
	:param string: Input value
	:type string: int, float, str, ...
	:rtype: bool
	'''
	if(isinstance(string, basestring)):
		return True
	else:
		return False

def daemonize(stdin="/dev/null", stdout="/dev/null", stderr="/dev/null"):
	'''
	Use this function to daemonize an existing script ::
	
		#!/usr/bin/env python
		from neuro.common import daemonize
		daemonize()
		while(True):
			...
	
	:param stdin: Location to redirect standard input
	:type stdin: str
	:param stdout: Location to redirect standard output
	:type stdout: str
	:param stderr: Location to redirect standard error
	:type stderr: str
	'''
	## --- first fork
	try:
		pid = os.fork()
		
		## --- exit first parent
		if(pid > 0):
			sys.exit(0)
	except OSError, e:
		sys.stderr.write("fork #1 failed: (%d) %sn" % (e.errno, e.strerror))
		sys.exit(1)
		
	## --- detach from parent environment
	os.chdir("/")
	os.umask(0)
	os.setsid()
	
	## --- second fork
	try:
		pid = os.fork()
		
		## ---  exit second parent
		if(pid > 0):
			sys.exit(0)
	except OSError, e:
		sys.stderr.write("fork #2 failed: (%d) %sn" % (e.errno, e.strerror))
		sys.exit(1)
		
	## --- redirect standard file descriptors.
	for f in sys.stdout, sys.stderr: f.flush()
	si = file(stdin, 'r')
	so = file(stdout, 'a+')
	se = file(stderr, 'a+', 0)
	os.dup2(si.fileno(), sys.stdin.fileno())
	os.dup2(so.fileno(), sys.stdout.fileno())
	os.dup2(se.fileno(), sys.stderr.fileno())

class Logger(Object):
	'''	
	.. note:: If you need more advanced logging capabilities, consider using python's :py:mod:`logging` module.
	'''
	_appLogger = None
		
	def __init__(self):
		'''
		Constructor ::
		
			logger = Logger()
		'''		
		self._destination = "screen"
		self._filename = "./nit.log"
		self._handle = None
		self._buffering = False
		self._buffer = ""
		self._debugger = Debugger()
		self._emailSettings = None

	def _log(self, message=""):
		'''
		Log the message
		
		:param message:
		:type message: str
		'''
		if(self._buffering):
			self._buffer += message
			return
		
		if(self._destination == "screen"):
			self._screen(message)
		elif(self._destination == "file"):
			self._file(message)
		elif(self._destination == "email"):
			self._email(message)
		elif(self._destination == "null"):
			return
			
	def _screen(self, message=""):
		'''
		Print message to screen
		
		:param message:
		:type message: str
		'''
		print(message),

	def _file(self, message=""):
		'''
		Log message to file
		
		:param message:
		:type message: str
		:rtype: bool
		'''
		try:
			if(self._handle != None):
				self._handle.write(message)
				return
		
			if(not filesystem.exists(self._filename)):
				filesystem.poke(self._filename, True)
				
		except Exception, e:
			self.setDestination("screen")
			self.warn(e.__str__())
			self._log(message)
			return
		
		if(not filesystem.isWritable(self._filename)):
			self.setDestination("screen")
			e = IOException(IOException.WRITE, self._filename)
			self.warn(e.__str__())
			self._log(message)
			return 

		self._handle = open(self._filename, "a")
		self._file(message)
		
	def _email(self, message=""):
		'''
		Log message to email
		
		:param message:
		:type message: str
		:raises: EmailException
		'''
		if(self._emailSettings == None):
			raise EmailException({}, "Email settings not yet set")
		
		self._emailSettings["body"] = message
		
		try:
			system.email(self._emailSettings)
		except Exception, e:
			self._destination = "screen"
			self.warn(e.__str__())
			self._log(message)
	
	@staticmethod
	def getAppLogger():
		'''
		Get application logger
		
		:rtype: :class:`Logger`
		'''
		if(Logger._appLogger == None):
			Logger._appLogger = Logger()

		Logger._appLogger.setDestination(config.logto)

		if(Logger._appLogger._destination == "file" and config.logfile != None
				and config.logfile != ""):
			Logger._appLogger.setFilename(config.logfile)
			
		return Logger._appLogger
	
	def setupEmail(self, settings):
		'''
		Setup email. Automatically toggles buffer and sets destination to email.
		
			>>> command.setupEmail({"to": "foo@bar.com", 
				"from": "your@email.com", "subject": "hey!", 
				"body": "this is a message"})
				
		:param settings:
		:type settings: dict
		'''
		if(not isinstance(settings, dict)):
			raise IllegalArgumentException("Email settings must be an instance of dict")
		
		self._emailSettings = settings
		
		if(not self._buffering):
			self._buffering = True

		self._destination = "email"
	
	def toggleBuffer(self):
		'''
		Toggle buffering on/off
		
			>>> logger.toggleBuffer()
			
		:rtype: bool
		'''
		self._buffering = not self._buffering
		
	def flush(self):
		'''
		Flush log buffer to configured destination
		
			>>> logger.flush()
			
		'''
		self._buffering = False
		self._log(self._buffer)
		self._buffer = ""
		self._buffering = True

	def info(self, message=""):
		'''
		Log an informational message
		
			>>> logger.info("Some message")
			
		:param message:
		:type message: str
		'''
		if(not isinstance(message, basestring)):
			message = message.__str__()
		
		self._log("[INFO]: " + message + "\n")
		
	def debug(self, message=""):
		'''
		Log debug information
		
			>>> logger.debug("Some message")

		:param message: Message to print to log
		:type message: str
		'''
		if not isinstance(message, basestring):
			message = message.__str__()
		
		if config.debug:
			self._log("[DEBUG]: " + message + "\n")
	
	def warn(self, message=""):
		'''
		Log a warning message
		
			>>> logger.warn("Some warning")
			
		:param message:
		:type message: str
		'''
		if(not isinstance(message, basestring)):
			message = message.__str__()
		
		self._log("[WARN]: " + message + "\n")
	
	def error(self, message=""):
		'''
		Log a warning message
		
			>>> logger.error("Some non-fatal error message")
			
		:param message:
		:type message: str
		'''
		log = "[ERROR]: "
		
		if(isinstance(message, Exception)):
			log = log + message.__str__()
		else:
			log = log + message

		log = log + "\n"

		if(config.debug):
			log = log + "\n" + self.getDebugMessage()

		self._log(log)

	def fatal(self, message="", status=1):
		'''
		Log a fatal message and exit program
		
			>>> logger.fatal("Fatal message")
			
		:param message:
		:type message: str, Exception
		:param status:
		:type status: int
		'''
		if(not isinstance(status, int)):
			raise IllegalArgumentException("Status must be an instance of int")

		log = "[FATAL]: "

		## --- get exception string
		if(isinstance(message, Exception)):
			log = log + message.__str__()
		else:
			log = log + message

		log = log + "\n"

		if(config.debug):
			dmesg = self.getDebugMessage()

			if(dmesg != ""):
				log = log + "\n" + self.getDebugMessage()
				
		if(self._buffering):
			self.flush()

		self._log(log)
				
		## --- exit now, don't just throw an exception like sys.exit()
		system.exit(status)

	def setDebugger(self, debugger):
		'''
		Set the debugger

		:param debugger:
		:type debugger: :class:`Debugger`
		'''
		if(not isinstance(debugger, Debugger)):
			raise  IllegalArgumentException("Parameter 'debugger' must be an instance of Debugger")

		self._debugger = debugger

	def setTrace(self, trace):
		'''
		Set debug message

		:param trace:
		:type trace: traceback
		:rtype: str
		'''
		self._trace = trace

	def getDebugMessage(self):
		'''
		Get debug message

		:rtype: str
		'''
		message = ""
		tab = ""

		## --- get exception and stack
		exceptionName = self._debugger.getExceptionName()
		stack = self._debugger.getTrace()

		## --- generate exception message
		if(exceptionName != None):
			message = "[EXCEPTION]:\n"
			message = message + exceptionName + "\n\n"

		## --- generate stack message
		if(stack != None):
			message = message + "[CALL STACK]:\n"

			for i in range(len(stack)):
				n = str(i + 1) + ".) "
				spacer = ""

				for j in range(len(n)):
					spacer += " "

				file = stack[i][0]
				line = str(stack[i][1])
				method = stack[i][2]

				message += tab + n + "File: " + file + "\n" + \
					spacer + tab + "  `-- Line: " + line + "\n" + \
					spacer + tab + "  `-- Method: " + method + "\n"

				tab += "  "
		
		return message

	def setException(self, exception):
		'''
		Set exception

		'''
		self._exception = exception

	def plain(self, message=""):
		'''
		Log a plain message
		
			>>> logger.plain("Plain message")
			
		:param message:
		:type message: str
		'''
		self._log(message)
			
	def setDestination(self, destination="screen"):
		'''
		Set log destination
		
			>>> logger.setDestination("screen")
			>>> logger.setDestination("email")
			>>> logger.setDestination("file")
			>>> logger.setDestination("null")
			
		:param destination: Log destination e.g. "screen", "email", "file"
		:type destination: str
		'''
		if(not isinstance(destination, basestring)):
			raise IllegalArgumentException("Destination must be an instance of str")
		
		if(destination == ""):
			raise IllegalArgumentException("Destination cannot be null")
		
		destination = destination.lower()
		
		if(destination == "screen"):
			self._destination = "screen"
		elif(destination == "email"):
			self._destination = "email"
		elif(destination == "file"):
			self._destination = "file"
		elif(destination == "null"):
			self._destination = "null"
		else:
			self.warn("Invalid log destination: " + destination)
			
	def setFilename(self, filename='./nit.log'):
		'''
		Set logger filename. Automatically sets destination to file.
		
			>>> logger.setFilename("/path/to/log.txt")
			
		:param filename:
		:type filename: str
		'''
		if(not isinstance(filename, basestring)):
			raise IllegalArgumentException("Filename must be an instance of str")

		filename = filename.strip()

		if(filename == ""):
			raise IllegalArgumentException("Filename cannot be empty")
		
		if(self._handle):
			self._handle.close()
			self._handle = None
			
		self._filename = filesystem.canonical(filename)

		self._destination = "file"

class Debugger(object):
	'''
	Debugger class
	'''

	def __init__(self):
		'''
		Constructor
		'''
		self._exception = sys.exc_info()[0]
		self._value = sys.exc_info()[1]
		self._trace = sys.exc_info()[2]

	def setTrace(self, trace):
		'''
		Set traceback

		:param trace:
		:type trace: traceback
		'''
#		if(not isinstance(trace, traceback)):
#			raise IllegalArgumentException("Parameter 'trace' must be an instance of traceback")

		self._trace = trace

	def setException(self, exception):
		'''
		Set exception

		:param exception:
		:type exception: Exception
		'''
		self._exception = exception

	def setValue(self, value):
		'''
		Debug message

		:param value:
		:type value: str
		'''
		if(not isinstance(value, basestring)):
			raise IllegalArgumentException("Value must be an instance of str")

		self._value = value


	def getTrace(self, format="list"):
		'''
		Get trace

		:rtype: traceback
		'''
		if(self._trace == None):
			self._trace = sys.exc_info()[2]
			
		if(format == "list"):
			trace = traceback.extract_tb(self._trace)

		if(len(trace) > 0):
			return trace
		else:
			return None

	def getException(self):
		'''
		Get exception

		:rtype: Exception
		'''
		if(self._exception == None):
			self._exception = sys.exc_info()[1]

		if(self._exception != None):
			return self._exception
		else:
			return None

	def getExceptionName(self):
		'''
		Get exception class name

		:rtype: str
		'''
		if(self._exception == None):
			self.getException()

		if(self._exception == None):
			return None
		else:
			return self._exception.__class__.__name__

	def getValue(self):
		'''
		Get debug value

		:rtype: str
		'''
		return self._value